datacops-cms
Version:
A modern, extensible CMS built with Next.js and Prisma.
220 lines (200 loc) • 8.38 kB
text/typescript
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable @typescript-eslint/ban-ts-comment */
import { PrismaClient } from "@prisma/client";
import fs from "fs";
import { getToken } from "next-auth/jwt";
import { NextRequest, NextResponse } from "next/server";
import path from "path";
const prisma = new PrismaClient();
const PERMISSIONS_DIR = path.resolve(process.cwd(), "content");
const PERMISSIONS_PATH = path.join(PERMISSIONS_DIR, "api-permissions.json");
// === Helper: Check permissions ===
export async function checkAllowed(type: string, method: string, request: NextRequest)
{
const token = await getToken({ req: request, secret: process.env.NEXTAUTH_SECRET });
if (token) {
if (token.role === "SUPERADMIN" || token.role === "ADMIN") return true;
}
if (!fs.existsSync(PERMISSIONS_PATH)) return true;
const perms = JSON.parse(fs.readFileSync(PERMISSIONS_PATH, "utf-8"));
return perms?.[type]?.[method] !== false;
}
// === Helper: Capitalize first letter for Prisma model ===
function modelName(type: string)
{
return type.charAt(0).toUpperCase() + type.slice(1);
}
// === Helper: Get relation fields for all content-types ===
function getAllRelationFieldsFromContentTypes(): Record<string, string[]>
{
const contentTypesFolder = path.resolve(process.cwd(), "content-types");
const result: Record<string, string[]> = {};
if (!fs.existsSync(contentTypesFolder)) return result;
const files = fs.readdirSync(contentTypesFolder).filter(f => f.endsWith(".json"));
for (const file of files) {
try {
const schema = JSON.parse(fs.readFileSync(path.join(contentTypesFolder, file), "utf-8"));
if (!schema.name || !Array.isArray(schema.fields)) continue;
const modelName = schema.name;
const relationFields = schema.fields
.filter((f: any) => f.type === "relation")
.map((f: any) => f.name);
result[modelName] = relationFields;
} catch { /* ignore errors */ }
}
return result;
}
// === GET: List all items for type, with optional relations ===
export async function GET(
req: NextRequest,
{ params }: { params: { type: string } }
)
{
const { type } = await params;
const allowed = await checkAllowed(type, "GET", req);
if (!allowed) {
return NextResponse.json({ error: "You are not allowed to perform this action" }, { status: 403 });
}
try {
const model = modelName(type);
const now = new Date();
const searchParams = new URL(req.url).searchParams;
const populateParam = searchParams.get("populate"); // e.g. "*", "projectTeam,author"
// --- Build dynamic "include" for Prisma ---
let include: Record<string, boolean> | undefined;
if (populateParam) {
const allRelationFields = getAllRelationFieldsFromContentTypes();
const modelRelationFields = allRelationFields[type] || [];
include = {};
if (populateParam === "*") {
modelRelationFields.forEach(field => { include![field] = true; });
} else {
for (const rel of populateParam.split(",").map(s => s.trim()).filter(Boolean)) {
if (modelRelationFields.includes(rel)) include[rel] = true;
}
}
if (Object.keys(include).length === 0) include = undefined;
}
// Auth
const token = await getToken({ req, secret: process.env.NEXTAUTH_SECRET });
let items;
if (token) {
// All data for logged-in user
// @ts-ignore
items = await prisma[model].findMany({ ...(include ? { include } : {}) });
} else {
// Published or scheduled-and-due only
// @ts-ignore
items = await prisma[model].findMany({
where: {
OR: [
{ status: "Published" },
{
status: "Scheduled",
schedule: { lte: now }
}
]
},
...(include ? { include } : {})
});
}
// Promote scheduled-to-due to published in DB and response
interface ScheduledItem
{
id: number | string;
status: string;
schedule?: string | Date | null;
[key: string]: any;
}
const scheduledToPublish: ScheduledItem[] = items.filter(
(item: ScheduledItem) => item.status === "Scheduled" && item.schedule && new Date(item.schedule) <= now
);
if (scheduledToPublish.length) {
await Promise.all(
scheduledToPublish.map(item =>
// @ts-ignore
prisma[model].update({
where: { id: item.id },
data: { status: "Published", schedule: null }
})
)
);
items = items.map((item: ScheduledItem): ScheduledItem =>
item.status === "Scheduled" && item.schedule && new Date(item.schedule) <= now
? { ...item, status: "Published", schedule: null }
: item
);
}
return NextResponse.json(items);
} catch (e: any) {
return NextResponse.json(
{ error: `Error fetching data for type "${params.type}": ${e.message}` },
{ status: 404 }
);
}
}
// === POST: Create a new item for type ===
export async function POST(
req: NextRequest,
{ params }: { params: { type: string } }
)
{
const { type } = await params;
const allowed = await checkAllowed(type, "POST", req);
if (!allowed) {
return NextResponse.json({ error: "You are not allowed to perform this action" }, { status: 403 });
}
if (!type) {
return NextResponse.json(
{ error: "Type parameter is required" },
{ status: 400 }
);
}
try {
const model = modelName(type);
const data = await req.json();
// Manage status/schedule logic
if (!data.status) data.status = "Draft";
if (data.status === "Published") {
data.schedule = null;
}
if (data.status === "Scheduled") {
if (!data.schedule) {
return NextResponse.json(
{ error: "Schedule date is required for Scheduled status." },
{ status: 400 }
);
}
const scheduleDate = new Date(data.schedule);
if (isNaN(scheduleDate.getTime()) || scheduleDate <= new Date()) {
return NextResponse.json(
{ error: "Schedule date must be a valid future date/time." },
{ status: 400 }
);
}
data.schedule = scheduleDate.toISOString();
} else {
data.schedule = null;
}
// === Relations (many-to-many, etc): Convert value to { connect: ... } if needed ===
// If you use the same trick in your frontend, you can skip this block.
// Otherwise, parse relation fields and wrap in { connect: [...] }
const allRelationFields = getAllRelationFieldsFromContentTypes();
const modelRelationFields = allRelationFields[type] || [];
for (const rel of modelRelationFields) {
// If relation field is array of IDs, convert to { connect: [{id}] }
if (Array.isArray(data[rel])) {
data[rel] = { connect: data[rel].map((id: string) => ({ id })) };
}
}
// @ts-ignore
const created = await prisma[model].create({ data });
return NextResponse.json(created, { status: 201 });
} catch (e: any) {
console.error("Error creating data:", e);
return NextResponse.json(
{ error: `Error creating data for type "${type}": ${e.message}` },
{ status: 400 }
);
}
}